index.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. import { useFlag, useParams } from 'common'
  2. import { ExternalLink, RefreshCw, Search, X } from 'lucide-react'
  3. import { useRouter } from 'next/router'
  4. import { parseAsString, parseAsStringLiteral, useQueryState } from 'nuqs'
  5. import React, { useMemo, useRef } from 'react'
  6. import { Button, Card, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from 'ui'
  7. import { Admonition } from 'ui-patterns'
  8. import { Input } from 'ui-patterns/DataInputs/Input'
  9. import { PageContainer } from 'ui-patterns/PageContainer'
  10. import {
  11. PageHeader,
  12. PageHeaderAside,
  13. PageHeaderDescription,
  14. PageHeaderMeta,
  15. PageHeaderSummary,
  16. PageHeaderTitle,
  17. } from 'ui-patterns/PageHeader'
  18. import { PageSection, PageSectionContent } from 'ui-patterns/PageSection'
  19. import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
  20. import { DeployEdgeFunctionButton } from '@/components/interfaces/EdgeFunctions/DeployEdgeFunctionButton'
  21. import {
  22. EDGE_FUNCTIONS_SORT_VALUES,
  23. EdgeFunctionsSort,
  24. EdgeFunctionsSortColumn,
  25. EdgeFunctionsSortDropdown,
  26. EdgeFunctionsSortOrder,
  27. } from '@/components/interfaces/EdgeFunctions/EdgeFunctionsSortDropdown'
  28. import { EdgeFunctionsListItem } from '@/components/interfaces/Functions/EdgeFunctionsListItem'
  29. import {
  30. FunctionsEmptyState,
  31. FunctionsInstructionsLocal,
  32. } from '@/components/interfaces/Functions/FunctionsEmptyState'
  33. import { TerminalInstructionsDialog } from '@/components/interfaces/Functions/TerminalInstructionsDialog'
  34. import { useFunctionsListShortcuts } from '@/components/interfaces/Functions/useFunctionsListShortcuts'
  35. import DefaultLayout from '@/components/layouts/DefaultLayout'
  36. import EdgeFunctionsLayout from '@/components/layouts/EdgeFunctionsLayout/EdgeFunctionsLayout'
  37. import AlertError from '@/components/ui/AlertError'
  38. import { DocsButton } from '@/components/ui/DocsButton'
  39. import { ShortcutTooltip } from '@/components/ui/ShortcutTooltip'
  40. import { useEdgeFunctionsQuery } from '@/data/edge-functions/edge-functions-query'
  41. import { useIsProjectActive } from '@/hooks/misc/useSelectedProject'
  42. import { DOCS_URL, IS_PLATFORM } from '@/lib/constants'
  43. import { onSearchInputEscape } from '@/lib/keyboard'
  44. import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
  45. import type { NextPageWithLayout } from '@/types'
  46. const EdgeFunctionsPage: NextPageWithLayout = () => {
  47. const router = useRouter()
  48. const { ref } = useParams()
  49. const showLastHourStats = useFlag('edgeFunctionsRequestMetrics')
  50. const isProjectActive = useIsProjectActive()
  51. const searchInputRef = useRef<HTMLInputElement>(null)
  52. const [search, setSearch] = useQueryState('search', parseAsString.withDefault(''))
  53. const [sort, setSortQueryParam] = useQueryState(
  54. 'sort',
  55. parseAsStringLiteral<EdgeFunctionsSort>(EDGE_FUNCTIONS_SORT_VALUES).withDefault('name:asc')
  56. )
  57. const {
  58. data: functions,
  59. error,
  60. isPending: isLoading,
  61. isError,
  62. isSuccess,
  63. isFetching,
  64. refetch,
  65. } = useEdgeFunctionsQuery({ projectRef: ref })
  66. useFunctionsListShortcuts({
  67. searchInputRef,
  68. setSearch,
  69. sort,
  70. setSort: setSortQueryParam,
  71. canCreateNew: isProjectActive,
  72. onCreateNew: () => router.push(`/project/${ref}/functions/new`),
  73. onRefresh: () => {
  74. refetch()
  75. },
  76. })
  77. const filteredFunctions = useMemo(() => {
  78. const temp = (functions ?? []).filter((x) =>
  79. x.name.toLowerCase().includes(search.toLowerCase())
  80. )
  81. const [sortCol, sortOrder] = sort.split(':') as [
  82. EdgeFunctionsSortColumn,
  83. EdgeFunctionsSortOrder,
  84. ]
  85. const orderMultiplier = sortOrder === 'asc' ? 1 : -1
  86. return temp.sort((a, b) => {
  87. if (sortCol === 'name') {
  88. return a.name.localeCompare(b.name) * orderMultiplier
  89. }
  90. if (sortCol === 'created_at') {
  91. return (a.created_at - b.created_at) * orderMultiplier
  92. }
  93. if (sortCol === 'updated_at') {
  94. return (a.updated_at - b.updated_at) * orderMultiplier
  95. }
  96. return 0
  97. })
  98. }, [functions, search, sort])
  99. const hasFunctions = (functions ?? []).length > 0
  100. return (
  101. <PageContainer size="large">
  102. <PageSection>
  103. <PageSectionContent>
  104. <div className="flex flex-col gap-6">
  105. {isLoading && <GenericSkeletonLoader />}
  106. {isError &&
  107. (IS_PLATFORM ? (
  108. <AlertError error={error} subject="Failed to retrieve edge functions" />
  109. ) : (
  110. <Admonition type="warning" title="Failed to retrieve edge functions">
  111. <p className="prose [&>code]:text-xs text-sm">
  112. Local functions can be found at <code>briven/functions</code> folder.
  113. </p>
  114. </Admonition>
  115. ))}
  116. {isSuccess && (
  117. <>
  118. {hasFunctions ? (
  119. <div className="space-y-4">
  120. <div className="flex items-center gap-2">
  121. <div className="flex items-center gap-2">
  122. <div className="relative">
  123. <ShortcutTooltip
  124. shortcutId={SHORTCUT_IDS.LIST_PAGE_FOCUS_SEARCH}
  125. label="Search functions"
  126. side="bottom"
  127. >
  128. <Input
  129. ref={searchInputRef}
  130. placeholder="Search function names"
  131. icon={<Search />}
  132. size="tiny"
  133. className="w-32 md:w-64"
  134. value={search}
  135. onChange={(event) => setSearch(event.target.value)}
  136. onKeyDown={onSearchInputEscape(search, setSearch)}
  137. actions={[
  138. search && (
  139. <Button
  140. key="clear"
  141. size="tiny"
  142. type="text"
  143. icon={<X />}
  144. onClick={() => setSearch('')}
  145. className="p-0 h-5 w-5"
  146. />
  147. ),
  148. ]}
  149. />
  150. </ShortcutTooltip>
  151. </div>
  152. </div>
  153. <div className="flex items-center gap-2">
  154. <EdgeFunctionsSortDropdown value={sort} onChange={setSortQueryParam} />
  155. </div>
  156. <ShortcutTooltip
  157. shortcutId={SHORTCUT_IDS.FUNCTIONS_LIST_REFRESH}
  158. side="bottom"
  159. >
  160. <Button
  161. type="default"
  162. icon={<RefreshCw />}
  163. loading={isFetching}
  164. onClick={() => refetch()}
  165. >
  166. Refresh
  167. </Button>
  168. </ShortcutTooltip>
  169. <span className="border-l border-default pl-2 text-xs text-foreground-light">
  170. {search && filteredFunctions.length !== functions.length
  171. ? `Viewing ${filteredFunctions.length} of ${functions.length} functions in total`
  172. : `Viewing ${functions.length} ${functions.length === 1 ? 'function' : 'functions'} in total`}
  173. </span>
  174. </div>
  175. <Card>
  176. <Table>
  177. <TableHeader>
  178. <TableRow>
  179. <TableHead>Name</TableHead>
  180. <TableHead>URL</TableHead>
  181. <TableHead className="hidden 2xl:table-cell">Created</TableHead>
  182. <TableHead className="lg:table-cell">Updated</TableHead>
  183. {showLastHourStats && (
  184. <>
  185. <TableHead className="lg:table-cell">Total requests (1h)</TableHead>
  186. <TableHead className="lg:table-cell">5xx error rate (1h)</TableHead>
  187. </>
  188. )}
  189. <TableHead className="hidden 2xl:table-cell">Deployments</TableHead>
  190. </TableRow>
  191. </TableHeader>
  192. <TableBody>
  193. <>
  194. {filteredFunctions.length > 0 ? (
  195. filteredFunctions.map((item) => (
  196. <EdgeFunctionsListItem key={item.id} function={item} />
  197. ))
  198. ) : (
  199. <TableRow>
  200. <TableCell colSpan={showLastHourStats ? 8 : 6}>
  201. <p className="text-sm text-foreground">No results found</p>
  202. <p className="text-sm text-foreground-light">
  203. Your search for "{search}" did not return any results
  204. </p>
  205. </TableCell>
  206. </TableRow>
  207. )}
  208. </>
  209. </TableBody>
  210. </Table>
  211. </Card>
  212. </div>
  213. ) : (
  214. <FunctionsEmptyState />
  215. )}
  216. </>
  217. )}
  218. {!IS_PLATFORM && <FunctionsInstructionsLocal />}
  219. </div>
  220. </PageSectionContent>
  221. </PageSection>
  222. </PageContainer>
  223. )
  224. }
  225. EdgeFunctionsPage.getLayout = (page: React.ReactElement) => {
  226. return (
  227. <DefaultLayout>
  228. <EdgeFunctionsLayout title="Edge Functions">
  229. <div className="w-full min-h-full flex flex-col items-stretch">
  230. <PageHeader size="large">
  231. <PageHeaderMeta>
  232. <PageHeaderSummary>
  233. <PageHeaderTitle>Edge Functions</PageHeaderTitle>
  234. <PageHeaderDescription>
  235. Run server-side logic close to your users
  236. </PageHeaderDescription>
  237. </PageHeaderSummary>
  238. <PageHeaderAside>
  239. <DocsButton href={`${DOCS_URL}/guides/functions`} />
  240. <Button asChild type="default" icon={<ExternalLink />}>
  241. <a
  242. target="_blank"
  243. rel="noreferrer"
  244. href="https://github.com/briven/briven/tree/master/examples/edge-functions/briven/functions"
  245. >
  246. Examples
  247. </a>
  248. </Button>
  249. {IS_PLATFORM && <DeployEdgeFunctionButton />}
  250. </PageHeaderAside>
  251. </PageHeaderMeta>
  252. </PageHeader>
  253. {page}
  254. </div>
  255. </EdgeFunctionsLayout>
  256. <TerminalInstructionsDialog />
  257. </DefaultLayout>
  258. )
  259. }
  260. export default EdgeFunctionsPage